@file:Suppress("ReturnCount", "ThrowsCount")

package com.gitee.wsl.doc.prosemirror.model


import com.gitee.wsl.doc.prosemirror.exception.RangeError
import com.gitee.wsl.doc.prosemirror.model.attr.Attrs
import com.gitee.wsl.doc.prosemirror.model.content.ContentMatch
import com.gitee.wsl.doc.prosemirror.model.content.ResolveCache
import com.gitee.wsl.doc.prosemirror.model.fragment.Fragment
import com.gitee.wsl.doc.prosemirror.model.mark.Mark
import com.gitee.wsl.doc.prosemirror.model.mark.MarkType
import com.gitee.wsl.doc.prosemirror.model.node.Node
import com.gitee.wsl.doc.prosemirror.model.node.NodeId
import com.gitee.wsl.doc.prosemirror.model.node.NodeType
import com.gitee.wsl.doc.prosemirror.model.node.inline.TextNode
import com.gitee.wsl.concurrent.collections.ConcurrentMutableMap
import kotlinx.serialization.json.JsonObject


// A document schema. Holds [node](#model.NodeType) and [mark type](#model.MarkType) objects for the
// nodes and marks that may occur in conforming documents, and provides functionality for creating
// and deserializing such documents.
//
// When given, the type parameters provide the names of the nodes and marks in this schema.
class Schema {
    // The [spec](#model.SchemaSpec) on which the schema is based, with the added guarantee that its
    // `nodes` and `marks` properties are [`OrderedMap`](https://github.com/marijnh/orderedmap)
    // instances (not raw objects).
    val spec: SchemaSpec

    // An object mapping the schema's node names to node type objects.
    val nodes: Map<String, NodeType> // {readonly [name in Nodes]: NodeType} & {readonly [key: string]: NodeType}

    // A map from mark names to mark type objects.
    val marks: Map<String, MarkType>

    // The [linebreak
    // replacement](#model.NodeSpec.linebreakReplacement) node defined
    // in this schema, if any.
    var linebreakReplacement: NodeType? = null

    /**
     * From some testing on mix-contents.json (will differ based on document etc.);
     * Initial Render: 3:1 iterating (reads) vs writing
     * Selection: ~10:1
     * Typing: ~5:1
     */
    val resolveCache = ConcurrentMutableMap<NodeId, ResolveCache>()

    // Construct a schema from a schema [specification](#model.SchemaSpec).
    constructor(spec: SchemaSpec) {
        this.spec = spec.copy(nodes = spec.nodes.toMap(), marks = spec.marks.toMap())
        this.nodes = NodeType.compile(this.spec.nodes, this)
        this.marks = MarkType.compile(this.spec.marks, this)

        val contentExprCache = mutableMapOf<String, ContentMatch>()
        for (prop in this.nodes.keys) {
            if (prop in this.marks) {
                throw RangeError("$prop can not be both a node and a mark")
            }
            val type = this.nodes[prop]!!
            val contentExpr = type.spec.content ?: ""
            val markExpr = type.spec.marks
            type.contentMatch = contentExprCache.getOrPut(contentExpr) {
                ContentMatch.parse(contentExpr, this.nodes)
            }
            type.inlineContent = type.contentMatch.inlineContent
            if (type.spec.linebreakReplacement == true) {
                if (linebreakReplacement != null) throw RangeError("Multiple linebreak nodes defined")
                if (!type.isInline || !type.isLeaf) {
                    throw RangeError("Linebreak replacement nodes must be inline leaf nodes")
                }
                linebreakReplacement = type
            }
            type.markSet = when {
                markExpr == "_" -> null
                !markExpr.isNullOrEmpty() -> gatherMarks( markExpr.split(" "))
                markExpr == "" || !type.inlineContent -> emptyList()
                else -> null
            }
        }
        this.marks.forEach { (prop, type) ->
            val excl = type.spec.excludes
            type.excluded = if (excl == null) {
                listOf(type)
            } else if (excl == "") {
                emptyList()
            } else {
                gatherMarks( excl.split(" "))
            }
        }

//        this.nodeFromJSON = this.nodeFromJSON.bind(this)
//        this.markFromJSON = this.markFromJSON.bind(this)
        this.topNodeType = this.nodes[this.spec.topNode ?: "doc"]!!
//        this.cached.wrappings = Object.create(null)
    }

    // The type of the [default top node](#model.SchemaSpec.topNode) for this schema.
    val topNodeType: NodeType

    // An object for storing whatever values modules may want to compute and cache per schema. (If
    // you want to store something in it, try to use property names unlikely to clash.)
    val cached = mutableMapOf<String, Any>()

    // Create a node in this schema. The `type` may be a string or a `NodeType` instance. Attributes
    // will be extended with defaults, `content` may be a `Fragment`, `null`, a `Node`, or an array
    // of nodes.
    fun node(type: NodeType, attrs: Attrs? = null, content: Fragment?, marks: List<Mark>?): Node {
        if (type.schema != this) {
            throw RangeError("Node type from different schema used (${type.name})")
        }

        return type.createChecked(attrs, content, marks)
    }

    fun node(type: NodeType, attrs: Attrs? = null, content: Node?, marks: List<Mark>?): Node {
        if (type.schema != this) {
            throw RangeError("Node type from different schema used (${type.name})")
        }

        return type.createChecked(attrs, content, marks)
    }

    fun node(type: NodeType, attrs: Attrs? = null, content: List<Node>?, marks: List<Mark>?): Node {
        if (type.schema != this) {
            throw RangeError("Node type from different schema used (${type.name})")
        }

        return type.create(attrs, content, marks)
    }

    fun node(type: String) = node(nodeType(type), null, null as Fragment?, null)

    fun node(type: String, attrs: Attrs? = null) = node(nodeType(type), attrs, null as Fragment?, null)

    fun node(type: String, attrs: Attrs? = null, content: Fragment?, marks: List<Mark>? = null) =
        node(nodeType(type), attrs, content, marks)

    fun node(type: String, attrs: Attrs? = null, content: Node?, marks: List<Mark>? = null) =
        node(nodeType(type), attrs, content, marks)

    fun node(type: String, attrs: Attrs? = null, content: List<Node>?, marks: List<Mark>? = null) =
        node(nodeType(type), attrs, content, marks)

    // Create a text node in the schema. Empty text nodes are not allowed.
    fun text(text: String, marks: List<Mark>? = null): Node {
        val type = this.nodes["text"]!!
        return TextNode(type, type.defaultAttrs, text, Mark.setFrom(marks))
    }

    // Create a mark with the given type and attributes.
    fun mark(type: String, attrs: Attrs? = null): Mark {
        return mark(marks[type]!!, attrs)
    }

    // Create a mark with the given type and attributes.
    fun mark(type: MarkType, attrs: Attrs? = null): Mark {
        return type.create(attrs)
    }

    // Deserialize a node from its JSON representation. This method is bound.
    fun nodeFromJSON(json: JsonObject?, withId: Boolean = false, check: Boolean = false): Node {
        return Node.fromJSON(this, json, withId, check)
    }

    // Deserialize a mark from its JSON representation. This method is bound.
    fun markFromJSON(json: JsonObject?, withId: Boolean = false, check: Boolean = false): Mark {
        return Mark.fromJSON(this, json, withId, check)
    }

    fun nodeType(name: String): NodeType {
        return this.nodes[name] ?: throw RangeError("Unknown node type: $name")
    }
}

@Suppress("NestedBlockDepth")
fun Schema.gatherMarks(marks: List<String>): List<MarkType> {
    val found = mutableListOf<MarkType>()
    for (name in marks) {
        val mark = this.marks[name]
        if (mark != null) {
            found.add(mark)
        } else {
            this.marks.forEach { (prop, mark) ->
                if (name == "_" || (mark.spec.group?.let { it.split(" ").indexOf(name) > -1 } == true)) {
                    found.add(mark)
                }
            }
        }
    }
    return found
}

